{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"../common/rfsoc_book_banner.jpg\" alt=\"University of Strathclyde\" align=\"left\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Notebook Set D\n",
    "\n",
    "---\n",
    "\n",
    "## 01 - Baseband Modulation\n",
    "In this notebook, we explore various baseband modulation schemes. These include Binary Phase Shift Keying (BPSK), Quadrature Phase Shift Keying (QPSK), and Quadrature Amplitude Modulation (QAM).\n",
    "\n",
    "## Table of Contents\n",
    "* [1. Introduction](#introduction)\n",
    "* [2. Binary Phase Shift Keying (BPSK)](#bpsk)\n",
    "* [3. Quadrature Phase Shift Keying (QPSK)](#qpsk)\n",
    "* [4. Quadrature Amplitude Modulation (QAM)](#qam)\n",
    "* [5. Conclusion](#conclusion)\n",
    "\n",
    "## Revision\n",
    "* **v1.0** | 05/12/22 | *First Revision*\n",
    "\n",
    "---\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Introduction <a class=\"anchor\" id=\"introduction\"></a>\n",
    "In the context of wireless communications, baseband modulation is distinct from, and takes place prior to, the modulation of a signals from baseband to Intermediate Frequency (IF) or Radio Frequency (RF). Baseband modulation is the mapping of data bits to symbols according to the baseband modulation scheme. In this notebook, we will specifically cover the Phase Shift Keying (PSK) and Quadrature Amplitude Modulation (QAM) schemes.\n",
    "\n",
    "For this notebook we will be using NumPy for computation and Matplotlib for matlab-like visualizations of our waveforms. We will import these libraries now."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Binary Phase Shift Keying (BPSK) <a class=\"anchor\" id=\"bpsk\"></a>\n",
    "\n",
    "One of the most common modulation schemes is BPSK, where each bit is assigned one symbol. We start by generating some random bits."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bits = np.random.randint(0,2,16)\n",
    "bits"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can define the scheme as a simple Python list and use list comprehension to map every bit to a symbol. Now our bits will be mapped to the desired symbols."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bpsk_scheme = [-1+0j, 1+0j]\n",
    "bpsk_symbols = [bpsk_scheme[i] for i in bits]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can plot our BPSK 'modulated' data on a constellation map. Notice that they appear as dots on the horizontal plane, as BPSK only includes the real (I) component. Changes between these symbols are equivalent to a phase change of 180 degrees."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(5,5))\n",
    "plt.scatter(np.real(bpsk_symbols), np.imag(bpsk_symbols))\n",
    "plt.title('BPSK constellation diagram')\n",
    "plt.xlabel('Channel 1 amplitude')\n",
    "plt.ylabel('Channel 2 amplitude')\n",
    "plt.grid()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Quadrature Phase Shift Keying (QPSK) <a class=\"anchor\" id=\"qpsk\"></a>\n",
    "\n",
    "Not all modulation schemes need to have 2 symbols. If we take advantage of the 2nd (Quadrature) channel, we can represent 2 bits per symbol using the Quaternary Phase Shift Keying (QPSK) modulation scheme. In order to generate QPSK, we have a similar scheme. Except this time we have 4 possible symbols, which use 2 bits per symbol."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "qpsk_scheme = [1+1j, 1-1j, -1+1j, -1-1j]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We perform the same mapping as we did earlier, but instead of generating random bits we can generate random integers from 0 to 3. These random integers represent our individual bit pairs i.e. 0 -> 00, 1 -> 01, 2 -> 10, 3 -> 11."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ints = np.random.randint(0,4,1024)\n",
    "ints"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Again, using list comprehension we can map every integer to a symbol, which creates a vector of symbols ready for the next stages of modulation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "qpsk_symbols = [qpsk_scheme[i] for i in ints]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, if we plot the QPSK symbols on a constellation diagram, as we did with BPSK, we can see 4 distinct states for each symbol."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(5,5))\n",
    "plt.scatter(np.real(qpsk_symbols), np.imag(qpsk_symbols))\n",
    "plt.title('QPSK constellation diagram')\n",
    "plt.xlabel('Channel 1 amplitude')\n",
    "plt.ylabel('Channel 2 amplitude')\n",
    "plt.grid()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Quadrature Amplitude Modulation (QAM) <a class=\"anchor\" id=\"qam\"></a>\n",
    "Let's repeat the same process with 16-QAM. This time, we are using a modulation scheme with 4 levels (or 4 bits per symbol), which means we will need to generate integers in the range of 0-15. Don't forget, when specifying ranges in Python (like the NumPy randint function) the last digit is not inclusive."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Generate 16-QAM symbols\n",
    "qam_scheme = [-3-3j, -3-1j, -3+3j, -3+1j,  \\\n",
    "              -1-3j, -1-1j, -1+3j, -1+1j,  \\\n",
    "               3-3j,  3-1j,  3+3j,  3+1j,  \\\n",
    "               1-3j,  1-1j,  1+3j,  1+1j]\n",
    "ints = np.random.randint(0,16,1024)\n",
    "qam_symbols = [qam_scheme[i] for i in ints]\n",
    "\n",
    "# Plot the mapped symbols\n",
    "plt.figure(figsize=(5,5))\n",
    "plt.scatter(np.real(qam_symbols), np.imag(qam_symbols))\n",
    "plt.title('16-QAM constellation diagram')\n",
    "plt.xlabel('Channel 1 amplitude')\n",
    "plt.ylabel('Channel 2 amplitude')\n",
    "plt.grid()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We would proceed similarly with 64-QAM, 128-QAM, etc. With good channel conditions we could go as high as 1024-QAM! The advantage of using higher order modulation schemes is that we can stuff more bits into a single symbol, which can drastically improve our data throughput. However, as we will soon find out later in the next notebook, this makes our modulated data more susceptible to noise."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. Conclusion <a class=\"anchor\" id=\"conclusion\"></a>\n",
    "In this notebook, we covered some of the common baseband modulation schemes. In the next notebook, we explore the Signal to Noise Ratio (SNR), the Error Vector Magnitude (EVM), and the Bit Error Rate (BER)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "[⬅️ Previous Notebook](../notebook_C/05_fast_fourier_transform.ipynb) || [Next Notebook 🚀](02_evm_and_ber.ipynb)\n",
    "\n",
    "Copyright © 2023 Strathclyde Academic Media\n",
    "\n",
    "---\n",
    "---"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
